package cn.demoncat.util.pay.wxpay;

import cn.demoncat.util.exception.BizRuntimeException;
import cn.demoncat.util.exception.IoRuntimeException;
import cn.demoncat.util.exception.ParamRuntimeException;
import cn.demoncat.util.http.HttpUtil;
import cn.demoncat.util.http.entity.HttpRequest;
import cn.demoncat.util.io.IoUtil;
import cn.demoncat.util.lang.*;
import cn.demoncat.util.lang.constant.DateConstant;
import cn.demoncat.util.lang.constant.StringConstant;
import cn.demoncat.util.lang.entity.Result;
import cn.demoncat.util.pay.wxpay.config.WxpayProperties;
import cn.demoncat.util.pay.wxpay.constant.WxpayConstant;
import cn.demoncat.util.pay.wxpay.constant.WxpayConstant.*;
import cn.demoncat.util.pay.wxpay.constant.WxpayLogConstant;
import cn.demoncat.util.pay.wxpay.constant.WxpayMsgConstant;
import cn.demoncat.util.pay.wxpay.entity.*;
import cn.demoncat.util.sec.HmacSHA256Util;
import cn.demoncat.util.sec.Md5Util;
import cn.demoncat.util.web.ServletUtil;
import cn.demoncat.util.xml.XmlUtil;
import cn.demoncat.util.xml.entity.XStreamClassAliasParam;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;


/**
 * 微信支付工具
 * 
 * 文档：https://pay.weixin.qq.com/wiki/doc/api/index.html
 * 费率：次日结算当日交易总和；普通行业费率 0.6%，特殊行业费率 1%（游戏、交易平台）
 * 
 * @author 延晓磊
 *
 * @since 2019年10月9日
 */
@Component
public class WxpayUtil {

	@Resource
	private WxpayProperties wxpayProperties;
	
	private static final Logger LOG = LoggerFactory.getLogger(WxpayUtil.class);


	/**
	 * 申请支付
	 *
	 * <pre>
	 * 1、用户下单
	 * 2、请求微信预支付（*）
	 * 3、将预支付结果返回到前端（*）
	 * 4、前端调起支付
	 * </pre>
	 *
	 * @param orderNo   订单号
	 * @param name      网站名称-商品名称
	 * @param amount    金额
	 * @param ip        用户IP
	 * @param openid    用户ID（JSAPI必传）
	 * @param tradeType	交易类型
	 */
	public Result pay(String orderNo, String name, BigDecimal amount, String ip, String openid, TradeType tradeType){
		// 统一下单
		WxpayParamPrepay param = new WxpayParamPrepay();
		param.setBody(name);
		param.setOut_trade_no(orderNo);
		param.setTotal_fee(amount.multiply(new BigDecimal(100)).intValue());
		param.setSpbill_create_ip(ip);
		param.setTrade_type(tradeType.getType());
		param.setOpenid(openid);
		WxpayResultPrepay result = this.payPre(param);
		// 返回前端
		return this.payResult(result);
	}
 
	/**
	 * 申请支付/统一下单(预支付)
	 * 
	 * <pre>
	 * 场景：app/js/native支付前，请求微信创建支付订单 > 保存微信支付订单信息 > 请求payPreResult将结果返回前端
	 * 注意：预支付订单有效期2小时
	 * 资质：开通微信商户的APP支付权限、JS支付权限、扫码支付权限
	 * PC支付流程：申请支付(S) > 生成二维码(S) > 展示二维码(C) > 扫码支付(U)
	 * PH支付流程：申请支付(S) > 调起微信客户端(C) > 微信客户端支付(U)
	 * </pre>
	 * 
	 * @param param
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月15日
	 */
	private WxpayResultPrepay payPre(WxpayParamPrepay param) {
		// 支付通知
		param.setNotify_url(wxpayProperties.getNotifyUrl());
		// 发起请求
		return request(Api.PAY_PRE, param, WxpayResultPrepay.class);
	}
 
	/**
	 * 申请支付/调起支付（返回前端）
	 * 
	 * <pre>
	 * 支付流程：
	 * 1、用户下单
	 * 2、服务端调用payPre > 请求微信 > 获取微信支付订单 > 将支付订单信息返回前端（根据trade_type返回不同的信息）
	 * 3、客户端根据支付订单信息，调起微信支付/展示支付二维码/跳转微信支付页面
	 * 4、用户通过微信支付(2小时内)
	 * 5、微信服务端请求notify_url，返回支付结果通知
	 * 6、客户端间隔请求服务端查询支付状态，刷新订单状态
	 * </pre>
	 * 
	 * @param param
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月16日
	 */
	private Result payResult(WxpayResultPrepay param) {
		// 根据支付类型分发
		TradeType tradeType = TradeType.getByType(param.getTrade_type());
		Map<String, String> map;
		switch (tradeType) {
		case NATIVE:
			// PC扫码支付：根据code_url生成二维码 > 用户扫码支付 > 页面查询服务端支付状态来刷新界面
			return Result.successStr(param.getCode_url());
		case APP:
			// APP支付：根据map调起微信支付 > 用户完成支付 > 返回来源页 > 页面查询服务端支付状态来刷新界面
			map = new HashMap<>();
			// 应用ID
			map.put("appid", wxpayProperties.getAppid());
			// 商户号
			map.put("partnerid", wxpayProperties.getMchid());
			// 预支付交易会话ID
			map.put("prepayid", param.getPrepay_id());
			// 扩展字段，固定值
			map.put("package", "Sign=WXPay");
			// 随机字符串
			map.put("noncestr", IDUtil.getUuid());
			// 时间戳(秒)
			map.put("timestamp", Long.toString(System.currentTimeMillis()/1000));
			// 签名
			map.put("sign", Md5Util.sign(map, wxpayProperties.getKey()));
			return Result.success(map);
		case MWEB:
			// H5支付：根据mweb_url跳转到微信支付页面 > 用户完成支付 > 返回来源页  > 页面查询服务端支付状态来刷新界面
			return Result.successStr(param.getMweb_url());
		case JSAPI:
			// 公众号/小程序支付：根据map调起微信支付 > 用户完成支付 > 返回来源页 > 页面查询服务端支付状态来刷新界面
			map = new HashMap<>();
			// 应用ID
			map.put("appId", wxpayProperties.getAppid());
			// 时间戳(秒)
			map.put("timeStamp", Long.toString(System.currentTimeMillis()/1000));
			// 随机字符串
			map.put("nonceStr", IDUtil.getUuid());
			// 签名类型
			map.put("signType", SignType.MD5);
			// 扩展字段：prepay_id=xxx
			map.put("package", "prepay_id=" + param.getPrepay_id());
			// 签名
			map.put("paySign", Md5Util.sign(map, wxpayProperties.getKey()));
			return Result.success(map);
		default:
			return Result.ERROR_PARAM_ILLEGAL;
		}
	}
	
	/**
	 * 查询支付订单
	 * 
	 * <pre>
	 * 场景：商户故障未收到支付通知；关闭订单时确认状态
	 * </pre>
	 * 
	 * @param param
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月15日
	 */
	public WxpayResultPayQuery queryPay(WxpayParamPayQuery param) {
		return request(Api.PAY_QUERY, param, WxpayResultPayQuery.class);
	}
	
	/**
	 * 关闭支付订单
	 * 
	 * <pre>
	 * 场景：商户订单支付失败需要生成新订单来发起支付时，要先关闭原支付订单，避免重复支付
	 * 场景：用户下单后支付超时，不再受理支付时，关闭订单
	 * 注意：支付订单创建5分钟后才能关闭
	 * </pre>
	 * 
	 * @param param
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月15日
	 */
	public WxpayResult closePay(WxpayParamPayClose param) {
		return request(Api.PAY_CLOSE, param, WxpayResult.class);
	}
	
	/**
	 * 申请退款
	 * 
	 * <pre>
	 * 场景：部分或全额退款
	 * 注意：交易时间超过一年的订单无法提交退款
	 * 注意：支持单笔交易分多次退款，总金额不能超过订单总额，多次退款需要提交原支付订单和不同的退款单号（重复提交使用原退款号）
	 * 注意：每秒钟正常的申请退款请求次数不超过150次，每秒钟异常或错误的退款申请请求不超过6次，每个支付订单的部分退款次数不能超过50次
	 * 注意：请求需要双向证书
	 * </pre>
	 * 
	 * @param param
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月15日
	 */
	public WxpayResultRefund refund(WxpayParamRefund param) {
		// 退款通知
		param.setNotify_url(wxpayProperties.getRefundNotifyUrl());
		return request(Api.REFUND, param, WxpayResultRefund.class);
	}

	/**
	 * 申请退款
	 *
	 * @param orderNo       订单号
	 * @param orderAmount   订单总额
	 * @param refundNo  	退款号
	 * @param refundAmount  退款额
	 * @param refundDesc    退款原因
	 *
	 * @author 延晓磊
	 * @since 2023年06月01日
	 */
	public WxpayResultRefund refund(String orderNo, BigDecimal orderAmount, String refundNo, BigDecimal refundAmount, String refundDesc){
		WxpayParamRefund param = new WxpayParamRefund();
		param.setOut_trade_no(orderNo);
		param.setOut_refund_no(refundNo);
		param.setTotal_fee(orderAmount.multiply(new BigDecimal(100)).intValue());
		param.setRefund_fee(refundAmount.multiply(new BigDecimal(100)).intValue());
		param.setRefund_desc(refundDesc);
		return this.refund(param);
	}

	/**
	 * 查询退款申请
	 * 
	 * <pre>
	 * 场景：提交退款申请后，通过调用该接口查询退款状态
	 * 注意：退款有一定延时，用零钱支付的退款20分钟内到账，银行卡支付的退款3个工作日后重新查询退款状态
	 * 注意：如果单个支付订单部分退款次数超过20次请使用退款单号查询
	 * </pre>
	 * 
	 * @param param
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月15日
	 */
	public WxpayResultRefundQuery queryRefund(WxpayParamRefundQuery param) {
		return request(Api.REFUND_QUERY, param, WxpayResultRefundQuery.class);
	}
	
	/**
	 * 下载对账单
	 * 
	 * <pre>
	 * 场景：下载历史交易清单
	 * 注意：微信侧未成功下单的交易不会出现在对账单中。支付成功后撤销的交易会出现在对账单中，跟原支付单订单号一致
	 * 注意：微信在次日9点启动生成前一天的对账单，建议商户10点后再获取
	 * 注意：对账单中涉及金额的字段单位为“元”
	 * 注意：对账单接口只能下载三个月以内的账单
	 * 注意：对账单是以商户号来生成的，如一个商户号与多个appid有绑定关系，则使用其中任何一个appid都可以请求下载对账单
	 * 注意：对账单中的appid取自交易时候提交的appid，与请求下载对账单时使用的appid无关
	 * </pre>
	 * 
	 * @param param
	 * @return 对账文本
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月15日
	 */
	public String bill(WxpayParamBill param) {
		// 校验账单日期
		checkBillDate(param.getBill_date());
		// 请求微信
		String xml = request(Api.DOWNLOAD_BILL, param);
		// 成功时返回对账文本
		if (XmlUtil.isXml(xml)) {
			return xml;
		}else {
			// 失败时返回XML
			response(WxpayResult.class, xml);
			return null;
		}
	}

	/**
	 * 解析对账单
	 * 
	 * @param billType	类型：BillType.code
	 * @param bill		账单文本
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月15日
	 */
	public List<WxpayBill> parseBill(String billType, String bill) {
		List<WxpayBill> list = new ArrayList<>();
		// 账单行：标题，数据...，统计标题，统计数据
		String[] lines = StringUtil.splitLine(bill);
		for (int i = 1; i < lines.length-2; i++) {
			// 解析账单：数据以逗号分隔，以`前缀
			list.add(parseBill(billType, StringUtil.split(lines[i])));
		}
		return list;
	}

	/**
	 * 下载资金流水账单
	 * 
	 * <pre>
	 * 场景：下载历史资金流水账单
	 * 注意：资金账单中的数据反映的是商户微信账户资金变动情况
	 * 注意：当日账单在次日上午9点开始生成，建议商户在上午10点以后获取
	 * 注意：资金账单中涉及金额的字段单位为“元”
	 * 注意：请求需要双向证书，HMAC-SHA256签名
	 * </pre>
	 * 
	 * @param param
	 * @return 账单文本
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月15日
	 */
	public String flow(WxpayParamFlow param) {
		// 校验账单日期
		checkBillDate(param.getBill_date());
		// 请求微信
		String xml = request(Api.DOWNLOAD_FLOW, param);
		// 成功时返回账单文本
		if (XmlUtil.isXml(xml)) {
			return xml;
		}else {
			// 失败时返回XML
			response(WxpayResult.class, xml);
			return null;
		}
	}
	
	/**
	 * 解析资金流水账单
	 * 
	 * @param accountType	类型：AccountType.code
	 * @param bill			账单文本
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月15日
	 */
	public List<WxpayFlow> parseFlow(String accountType, String bill) {
		List<WxpayFlow> list = new ArrayList<>();
		// 账单行：标题，数据...，统计标题，统计数据
		String[] lines = StringUtil.splitLine(bill);
		for (int i = 1; i < lines.length-2; i++) {
			// 解析账单：数据以逗号分隔，以`前缀
			list.add(parseFlow(accountType, StringUtil.split(lines[i])));
		}
		return list;
	}
	
	/**
	 * 支付通知（被动） - 响应微信后处理，处理失败不会重推
	 * 
	 * <pre>
	 * 场景：支付完成后，微信会请求notify_url（预支付时设置）把相关支付结果发送给商户
	 * 注意：微信会重复发送通知，商户系统须做好去重处理
	 * 注意：微信会重复发送通知（24H内以渐增的频率），直至收到成功应答
	 * 注意：如果没有正确收到支付通知，可以主动调用支付查询接口来确认订单状态
	 * 注意：商户系统须对通知信息验签、验参数，防止伪造的请求，然后再判断数据状态并加锁，防止重复处理
	 * </pre>
	 * 
	 * @param request
	 * @param response
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月16日
	 */
	public WxpayNotifyPay payNotify(HttpServletRequest request, HttpServletResponse response) {
		try {
			// 读取请求
			WxpayNotifyPay result = this.getPayNotifyResult(request);
			// 微信通知成功
			notifySuccess(response);
			return result;
		} catch (Exception e) {
			// 微信通知失败重推
			throw notifyError(response, e);
		}
	}

	/**
	 * 支付通知（被动） - 处理后响应微信，需控制处理时长和并发推送
	 *
	 * <pre>
	 * 场景：支付完成后，微信会请求notify_url（预支付时设置）把相关支付结果发送给商户
	 * 注意：微信会重复发送通知，商户系统须做好去重处理
	 * 注意：微信会重复发送通知（24H内以渐增的频率），直至收到成功应答
	 * 注意：如果没有正确收到支付通知，可以主动调用支付查询接口来确认订单状态
	 * 注意：商户系统须对通知信息验签、验参数，防止伪造的请求，然后再判断数据状态并加锁，防止重复处理
	 * </pre>
	 *
	 * @param request
	 * @param response
	 * @param consumer	业务处理
	 * @return
	 *
	 * @author 延晓磊
	 *
	 * @since 2019年10月16日
	 */
	public void payNotify(HttpServletRequest request, HttpServletResponse response, Consumer<WxpayNotifyPay> consumer) {
		try {
			// 读取请求
			WxpayNotifyPay result = this.getPayNotifyResult(request);
			// 业务处理
			consumer.accept(result);
			// 微信通知成功
			notifySuccess(response);
		} catch (Exception e) {
			// 微信通知失败重推
			throw notifyError(response, e);
		}
	}

	/**
	 * 退款通知（被动） - 响应微信后处理，处理失败不会重推
	 * 
	 * <pre>
	 * 场景：退款完成后，微信会请求notify_url（申请退款时设置）把相关退款结果发送给商户
	 * 注意：微信会重复发送通知，商户系统须做好去重处理
	 * 注意：微信会重复发送通知（24H内以渐增的频率），直至收到成功应答
	 * 注意：如果没有正确收到退款通知，可以主动调用退款查询接口来确认订单状态
	 * 注意：商户系统须对通知信息验签、验参数，防止伪造的请求，然后再判断数据状态并加锁，防止重复处理
	 * 注意：退款结果对重要的数据进行了加密，商户需要用商户秘钥进行解密后才能获得结果通知的内容 
	 * </pre>
	 * 
	 * @param request
	 * @param response
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月16日
	 */
	public WxpayNotifyRefund refundNotify(HttpServletRequest request, HttpServletResponse response) {
		try {
			// 读取请求
			WxpayNotifyRefund result = this.getRefundNotifyResult(request);
			// 微信通知成功
			notifySuccess(response);
			return result;
		} catch (Exception e) {
			// 微信通知失败重推
			throw notifyError(response, e);
		}
	}

	/**
	 * 退款通知（被动） - 处理后响应微信，需控制处理时长和并发推送
	 *
	 * <pre>
	 * 场景：退款完成后，微信会请求notify_url（申请退款时设置）把相关退款结果发送给商户
	 * 注意：微信会重复发送通知，商户系统须做好去重处理
	 * 注意：微信会重复发送通知（24H内以渐增的频率），直至收到成功应答
	 * 注意：如果没有正确收到退款通知，可以主动调用退款查询接口来确认订单状态
	 * 注意：商户系统须对通知信息验签、验参数，防止伪造的请求，然后再判断数据状态并加锁，防止重复处理
	 * 注意：退款结果对重要的数据进行了加密，商户需要用商户秘钥进行解密后才能获得结果通知的内容
	 * </pre>
	 *
	 * @param request
	 * @param response
	 * @param consumer	业务处理
	 * @return
	 *
	 * @author 延晓磊
	 *
	 * @since 2019年10月16日
	 */
	public void refundNotify(HttpServletRequest request, HttpServletResponse response, Consumer<WxpayNotifyRefund> consumer) {
		try {
			// 读取请求
			WxpayNotifyRefund result = this.getRefundNotifyResult(request);
			// 业务处理
			consumer.accept(result);
			// 微信通知成功
			notifySuccess(response);
		} catch (Exception e) {
			// 微信通知失败重推
			throw notifyError(response, e);
		}
	}

	public static void main(String[] args) {
		System.out.println(RandomStringUtils.randomAlphanumeric(32));
		System.out.println(RandomStringUtils.randomAlphanumeric(32));
	}
	
	
	/**
	 * 发起请求，返回响应
	 * 
	 * 注：对账单结果先判断是否为xml，非xml时表示成功，是xml时再进行校验让报错，不能直接调用本方法
	 * 
	 * @param api	请求的api
	 * @param param	请求参数
	 * @param clazz	响应类型
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月15日
	 */
	private <T extends WxpayResult>T request(Api api, WxpayParam param, Class<T> clazz) {
		return response(clazz, request(api, param));
	}
	
	/**
	 * 发起请求
	 * 
	 * @param api	请求的api
	 * @param param	请求参数
	 * 
	 * @return	xml/账单文本
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月15日
	 */
	private String request(Api api, WxpayParam param) {
		LOG.info(WxpayLogConstant.REQUEST, api.getName());
		// 赋值公共参数
		param.setAppid(wxpayProperties.getAppid());
		param.setMch_id(wxpayProperties.getMchid());
		param.setNonce_str(IDUtil.getUuid());
		// 签名类型
		if (Api.DOWNLOAD_FLOW.getCode().equals(api.getCode())) {
			// 资金流水账单HMAC-SHA256
			param.setSign_type(SignType.SHA256);
		}else {
			// 其它统一MD5
			param.setSign_type(SignType.MD5);
		}
		// 签名
		if (SignType.MD5.equals(param.getSign_type())) {
			param.setSign(Md5Util.sign(ReflectUtil.toStrMap(param), wxpayProperties.getKey()));
		}else {
			param.setSign(HmacSHA256Util.sign(ReflectUtil.toStrMap(param), wxpayProperties.getKey()));
		}
		// 转换为XML
		String xml = XmlUtil.toXml(param, WxpayConstant.XML_ROOT);
		// 请求微信 
		HttpRequest req = HttpRequest.post(api.getUrl()).xml(xml);
		if (Api.REFUND.getCode().equals(api.getCode()) || Api.DOWNLOAD_FLOW.getCode().equals(api.getCode())) {
			// 退款、资金流水使用SSL双向证书
			try {
				req.sslByPfxCerts(new FileInputStream(wxpayProperties.getCert()), wxpayProperties.getMchid());
			} catch (FileNotFoundException e) {
				throw new IoRuntimeException(WxpayLogConstant.READ_CERT_ERROR + wxpayProperties.getCert(), e);
			}
		}
		return HttpUtil.request(req).asString();
	}
	
	/**
	 * 校验并返回响应结果
	 * 
	 * 注：对账单结果先判断是否为xml，非xml时表示成功，是xml时再进行校验让报错
	 * 
	 * @param clazz
	 * @param xml
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月15日
	 */
	private <T extends WxpayResult>T response(Class<T> clazz, String xml) {
		// 解析响应
		T result = XmlUtil.toObj(xml, new XStreamClassAliasParam(clazz, WxpayConstant.XML_ROOT));
		// 判断响应码
		if (StringUtils.isBlank(result.getReturn_code()) || WxpayConstant.ResponseCode.FAIL.equals(result.getReturn_code())) {
			LOG.error(WxpayLogConstant.RESPONSE_ERROR, xml);
			throw new BizRuntimeException(WxpayMsgConstant.RESPONSE_ERROR);
		}
		// 判断业务码
		if (StringUtils.isBlank(result.getResult_code()) || WxpayConstant.ResponseCode.FAIL.equals(result.getResult_code())) {
			LOG.error(WxpayLogConstant.RESPONSE_ERROR_RESULT, xml);
			throw new BizRuntimeException(WxpayMsgConstant.RESPONSE_ERROR_RESULT + result.getErr_code_des());
		}
		// 判断签名：资金流水账单HMAC-SHA256签名但不需要验签，其它统一MD5签名
		if (StringUtils.isBlank(result.getSign()) || !result.getSign().equals(Md5Util.sign(XmlUtil.toMapStr(xml), wxpayProperties.getKey()))) {
			LOG.error(WxpayLogConstant.RESPONSE_ERROR_SIGN, xml);
			throw new BizRuntimeException(WxpayMsgConstant.RESPONSE_ERROR_SIGN);
		}
		return result;
	}

	/**
	 * 对账日期校验：3个月以内，必须是次日10点后
	 * 
	 * @param date
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月8日
	 */
	private static void checkBillDate(String date) {
		// 账单日
		LocalDate billDate = LocalDate.parse(date,DateConstant.DATE_FORMAT_NUM);
		// 昨日之前，3个月内
		LocalDate nowDate = LocalDate.now();
		if (!billDate.isBefore(nowDate) || LocalDateUtil.betweenMonth(billDate, nowDate) >= 3) {
			throw new ParamRuntimeException(WxpayMsgConstant.BILL_DATA_ERROR_MONTH);
		}
		// 次日10点后：当前 > 账单日 + 1天 + 10时
		if (!LocalDateTime.now().isAfter(billDate.plusDays(1).atTime(10,0))) {
			throw new ParamRuntimeException(WxpayMsgConstant.BILL_DATA_ERROR_DATE);
		}
	}
	
	/**
	 * 获取对账单字段值
	 * 
	 * @param rows
	 * @param index
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月16日
	 */
	private static String getBillCell(String[] rows, int index) {
		// 获取字段
		String cell = ArrayUtil.get(rows, index);
		// 去除前缀`
		if (StringUtils.isNotBlank(cell)) {
			return cell.replace(StringConstant.LEFT_1, StringConstant.EMPTY);
		}
		return cell;
	}
	
	/**
	 * 解析账单行数据
	 * 
	 * @param billType	类型：BillType.code
	 * @param rows		行数据
	 * 
	 * @author 延晓磊
	 * @return 
	 *
	 * @since 2019年10月16日
	 */
	private static WxpayBill parseBill(String billType, String[] rows) {
		WxpayBill bill = new WxpayBill();
		bill.setType(billType);
		bill.setTrade_time(getBillCell(rows, 0));
		bill.setAppid(getBillCell(rows, 1));
		bill.setMch_id(getBillCell(rows, 2));
		bill.setMch_id2(getBillCell(rows, 3));
		bill.setTransaction_id(getBillCell(rows, 5));
		bill.setOut_trade_no(getBillCell(rows, 6));
		bill.setOpenid(getBillCell(rows, 7));
		bill.setTrade_type(getBillCell(rows, 8));
		bill.setTrade_state(getBillCell(rows, 9));
		bill.setTotal_fee(MoneyUtil.toFen(getBillCell(rows, 12)).toString());
		if (BillType.PAY.getCode().equals(billType)) {
			// 支付账单
			bill.setBody(getBillCell(rows, 14));
			bill.setAttach(getBillCell(rows, 15));
			bill.setFee(MoneyUtil.toFen(getBillCell(rows, 16)).toString());
			bill.setFee_rate(getBillCell(rows, 17));
		}else {
			// 退款账单
			bill.setRefund_time(getBillCell(rows, 14));
			bill.setRefund_success_time(getBillCell(rows, 15));
			bill.setRefund_id(getBillCell(rows, 16));
			bill.setOut_refund_no(getBillCell(rows, 17));
			bill.setRefund_fee(MoneyUtil.toFen(getBillCell(rows, 18)).toString());
			bill.setRefund_status(getBillCell(rows, 21));
			bill.setBody(getBillCell(rows, 22));
			bill.setAttach(getBillCell(rows, 23));
			bill.setFee(MoneyUtil.toFen(getBillCell(rows, 24)).toString());
			bill.setFee_rate(getBillCell(rows, 25));
		}
		return bill;
	}
	
	/**
	 * 解析资金流水账单行数据
	 * 
	 * @param accountType	类型：AccountType.code
	 * @param rows			行数据
	 * 
	 * @author 延晓磊
	 * @return 
	 *
	 * @since 2019年10月16日
	 */
	private static WxpayFlow parseFlow(String accountType, String[] rows) {
		WxpayFlow bill = new WxpayFlow();
		bill.setType(accountType);
		bill.setFlowTime(getBillCell(rows, 0));
		bill.setWxpayNo(getBillCell(rows, 1));
		bill.setFlowNo(getBillCell(rows, 2));
		bill.setBizName(getBillCell(rows, 3));
		bill.setBizType(getBillCell(rows, 4));
		bill.setBizNo(getBillCell(rows, 10));
		bill.setFlowType(FlowType.getByName(getBillCell(rows, 5)).getCode());
		bill.setFlowAmount(MoneyUtil.toFen(getBillCell(rows, 6)).toString());
		bill.setAccountBalance(MoneyUtil.toFen(getBillCell(rows, 7)).toString());
		bill.setApplyUser(getBillCell(rows, 8));
		bill.setRemarks(getBillCell(rows, 9));
		return bill;
	}

	/**
	 * 读取支付通知的请求数据
	 *
	 * @param request
	 * @return
	 *
	 * @author 延晓磊
	 *
	 * @since 2019年10月16日
	 */
	public WxpayNotifyPay getPayNotifyResult(HttpServletRequest request) throws IOException {
		// 读取请求
		String xml = IoUtil.readToStr(request.getInputStream());
		// 验签并获取结果
		return response(WxpayNotifyPay.class, xml);
	}

	/**
	 * 读取退款通知的请求数据
	 *
	 * @param request
	 * @return
	 *
	 * @author 延晓磊
	 *
	 * @since 2019年10月16日
	 */
	private WxpayNotifyRefund getRefundNotifyResult(HttpServletRequest request) throws IOException {
		// 读取请求
		String xml = IoUtil.readToStr(request.getInputStream());
		// 解析响应
		WxpayNotifyRefund result = XmlUtil.toObj(xml, new XStreamClassAliasParam(WxpayNotifyRefund.class, WxpayConstant.XML_ROOT));
		// 判断响应码
		if (StringUtils.isBlank(result.getReturn_code()) || WxpayConstant.ResponseCode.FAIL.equals(result.getReturn_code())) {
			LOG.error(WxpayLogConstant.RESPONSE_ERROR, xml);
			throw new BizRuntimeException(WxpayMsgConstant.RESPONSE_ERROR);
		}
		// 解密退款数据
		result.setInfo(this.decodeRefundInfo(result.getReq_info()));
		return result;
	}

	/**
	 * 解密退款数据
	 * 
	 * @param reqInfo
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月16日
	 */
	private WxpayNotifyRefundInfo decodeRefundInfo(String reqInfo) {
		// 商户密钥MD5加密并小写
		String key = Md5Util.digest(wxpayProperties.getKey()).toLowerCase();
		// 用key对加密串做AES-256-ECB（PKCS7Padding）解密
		try {
			Security.addProvider(new BouncyCastleProvider());
			SecretKeySpec sk = new SecretKeySpec(StringUtil.getBytes(key), "AES");
			Cipher cipher = Cipher.getInstance("AES/ECB/PKCS7Padding");
			cipher.init(Cipher.DECRYPT_MODE, sk);
			reqInfo = StringUtil.getStr(cipher.doFinal(Base64.decodeBase64(reqInfo.getBytes(StandardCharsets.UTF_8))));
		} catch (Exception e) {
			throw new RuntimeException("解密失败", e);
		}
		// 转换退款信息
		return XmlUtil.toObj(reqInfo, new XStreamClassAliasParam(WxpayNotifyRefundInfo.class, "root"));
	}

	/**
	 * 成功通知响应
	 *
	 * @param response
	 *
	 * @author 延晓磊
	 *
	 * @since 2019年10月16日
	 */
	private static void notifySuccess(HttpServletResponse response){
		ServletUtil.responseText(response, WxpayConstant.RESPOND_XML_SUCCESS);
	}

	/**
	 * 错误通知响应
	 *
	 * @param response
	 * @return exception for throw
	 *
	 * @author 延晓磊
	 *
	 * @since 2019年10月16日
	 */
	private static BizRuntimeException notifyError(HttpServletResponse response, Exception e){
		ServletUtil.responseText(response, WxpayConstant.RESPOND_XML_FAIL);
		return new BizRuntimeException(WxpayMsgConstant.NOTIFY_READ_ERROR, e);
	}
}
